package com.tools.web.work.qr;

import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.tools.common.exception.IORuntimeException;
import com.tools.common.io.CharsetEnum;
import com.tools.common.io.IOKit;
import com.tools.common.object.Note;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * 二维码实体类
 * */
@Note("二维码实体类")
public final class QR {

    @Note("宽度")
    private int width = 100;

    @Note("高度")
    private int height = 100;

    @Note("二维码边距，越大距离边框越远")
    private int margin = 1;

    @Note("容错等级。越高意味着丢失更多的数据都能识别，但相应地可存储的数据就变少了。系统默认为 L。" +
            "【L】: 7% 容错率。即使丢失 7% 的数据也能正常识别" +
            "【M】: 15%" +
            "【Q】: 25%" +
            "【H】: 30%" +
            "这里设置默认为 Q。容错等级越高生成的二维码越分散。" )
    private ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.Q;

    @Note("二维码内容")
    private String content;

    @Note("二维码内容编码的字符集")
    private CharsetEnum contentCharset = CharsetEnum.UTF_8;

    @Note("二维码的前景色，默认为黑色。用十六进制表示颜色")
    private int frontColor = 0x000000;

    @Note("二维码的背景色，默认为白色。用十六进制表示颜色")
    private int backgroundColor = 0xFFFFFF;

    @Note("解析二维码的结果就放在这里")
    private Result decodeResult;

    /* ********************************************************************************************
     *
     *          构造器
     *
     * *********************************************************************************************
     * */

    public QR(String content) {
        this.setContent(content);
    }

    public QR(String content, int width, int height) {
        this.setContent(content);
        this.setWidth(width);
        this.setHeight(height);
    }

    public QR(InputStream qrImage) {
        this.decodeResult = this.read(qrImage);
    }

    public QR(InputStream qrImage, CharsetEnum contentCharset) {
        this.setContentCharset(contentCharset);
        this.decodeResult = this.read(qrImage);
    }

    public static QR of(String content) {
        return new QR(content);
    }

    public static QR of(String content, int width, int height) {
        return new QR(content, width, height);
    }

    public static QR of(InputStream qrImage) {
        return new QR(qrImage);
    }

    public static QR of(InputStream qrImage, CharsetEnum contentCharset) {
        return new QR(qrImage, contentCharset);
    }

    /* ********************************************************************************************
     *
     *          Getter / Setter
     *
     * *********************************************************************************************
     * */

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = (width < 0) ? this.width : width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = (height < 0) ? this.height : height;
    }

    public int getMargin() {
        return margin;
    }

    public void setMargin(int margin) {
        this.margin = margin;
    }

    public ErrorCorrectionLevel getErrorCorrectionLevel() {
        return errorCorrectionLevel;
    }

    public void setErrorCorrectionLevel(ErrorCorrectionLevel errorCorrectionLevel) {
        if(errorCorrectionLevel == null) throw new NullPointerException("不能设置 null 的容错策略");
        this.errorCorrectionLevel = errorCorrectionLevel;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = (content == null) ? "" : content;
    }

    public CharsetEnum getContentCharset() {
        return contentCharset;
    }

    public void setContentCharset(CharsetEnum contentCharset) {
        if(contentCharset == null) throw new NullPointerException("不能设置 null 的字符集来编码二维码内容");
        this.contentCharset = contentCharset;
    }

    public int getFrontColor() {
        return frontColor;
    }

    public void setFrontColor(int frontColor) {
        this.frontColor = frontColor;
    }

    public int getBackgroundColor() {
        return backgroundColor;
    }

    public void setBackgroundColor(int backgroundColor) {
        this.backgroundColor = backgroundColor;
    }

    public Result getDecodeResult() {
        return decodeResult;
    }

    /* ********************************************************************************************
     *
     *          链式方法
     *
     * *********************************************************************************************
     * */

    public QR width(int width) {
        this.setWidth(width);
        return this;
    }

    public QR height(int height) {
        this.setHeight(height);
        return this;
    }

    public QR margin(int margin) {
        this.setMargin(margin);
        return this;
    }

    public QR errorCorrectionLevel(ErrorCorrectionLevel level) {
        this.setErrorCorrectionLevel(level);
        return this;
    }

    public QR content(String content) {
        this.setContent(content);
        return this;
    }

    public QR contentCharset(CharsetEnum contentCharset) {
        this.setContentCharset(contentCharset);
        return this;
    }

    public QR frontColor(int frontColor) {
        this.setFrontColor(frontColor);
        return this;
    }

    public QR backgroundColor(int backgroundColor) {
        this.setBackgroundColor(backgroundColor);
        return this;
    }


    /* ********************************************************************************************
     *
     *          生成二维码图片
     *
     * *********************************************************************************************
     * */


    @Note("生成只有前景色、背景色的简单二维码")
    public BufferedImage out() {
        return this.out((QRColorFun) null);
    }


    @Note("生成自定义颜色的二维码")
    public BufferedImage out(QRColorFun fun) {
        Map<EncodeHintType, Object> hints = new HashMap<>(5);
        hints.put(EncodeHintType.ERROR_CORRECTION, errorCorrectionLevel);
        hints.put(EncodeHintType.CHARACTER_SET, contentCharset.getValue());
        hints.put(EncodeHintType.MARGIN, margin);
        try {
            BitMatrix qrCode = new MultiFormatWriter().
                    encode(content, BarcodeFormat.QR_CODE, width, height, hints);
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
            if(fun == null) {
                for (int x = 0; x < width; x++) {
                    for (int y = 0; y < height; y++) {
                        image.setRGB(x, y, qrCode.get(x, y) ? frontColor : backgroundColor);
                    }
                }
                return image;
            }
            for (int x = 0; x < width; x++) {
                for (int y = 0; y < height; y++) {
                    int color = fun.getColorInt(qrCode, x, y, width, height, frontColor, backgroundColor);
                    image.setRGB(x, y, color);
                }
            }
            return image;
        } catch (WriterException e) {
            throw new IORuntimeException(e.getMessage());
        }
    }


    @Note("生成中间带 logo 的二维码")
    public BufferedImage out(InputStream logo) {
        return this.out(logo, null);
    }


    @Note("生成中间带 logo 的自定义颜色的二维码")
    public BufferedImage out(InputStream logo, QRColorFun fun) {
        if(logo == null) throw new NullPointerException("logo 图片为 null");
        BufferedImage logoImage = IOKit.readImage(logo);
        private_checkLogoImage(logoImage);
        BufferedImage qrImage = this.out(fun);
        //logo 居中摆放
        int logoImageWidth = logoImage.getWidth();
        int logoImageHeight = logoImage.getHeight();
        int logoX = (qrImage.getWidth() - logoImageWidth) / 2;
        int logoY = (qrImage.getHeight() - logoImageHeight) / 2;
        //在二维码画布上绘画
        Graphics2D graphics2D = qrImage.createGraphics();
        graphics2D.drawImage(logoImage, logoX, logoY, logoImageWidth, logoImageHeight, null);
        Shape shape = new RoundRectangle2D.Float(logoX, logoY, logoImageWidth, logoImageHeight, 6, 6);
        graphics2D.setStroke(new BasicStroke(3f));
        graphics2D.draw(shape);
        graphics2D.dispose();
        return qrImage;
    }

    /* ********************************************************************************************
     *
     *          解析二维码图片
     *
     * *********************************************************************************************
     * */


    @Note("读取并解析二维码的字节输入流，将其中内容和元数据封装为 Result 对象返回")
    public Result read(InputStream qrImage) {
        if(qrImage == null) throw new NullPointerException("目标二维码字节输入流为 null");
        try {
            BufferedImage image = ImageIO.read(qrImage);
            BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);
            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
            HashMap<DecodeHintType, Object> hints = new HashMap<>(2);
            hints.put(DecodeHintType.CHARACTER_SET, contentCharset.getValue());
            return new MultiFormatReader().decode(bitmap, hints);
        } catch (Exception e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("读取并解析二维码图片文件实例，将其中内容和元数据封装为 Result 对象返回")
    public Result read(File qrImage) {
        FileInputStream qrImageInput = IOKit.fileInputStream(qrImage);
        return read(qrImageInput);
    }


    @Note("根据二维码图片文件的路径获取它的文件实例，将其中内容和元数据封装为 Result 对象返回")
    public Result read(String qrImagePath) {
        FileInputStream qrImageInput = IOKit.fileInputStream(qrImagePath);
        return read(qrImageInput);
    }

    /* ********************************************************************************************
     *
     *          私有逻辑
     *
     * *********************************************************************************************
     * */

    @Note("校验 logo 的宽高，logo 的宽高应当是二维码宽高的 1/3")
    private void private_checkLogoImage(BufferedImage logoImage) {
        if(logoImage.getWidth() * 3 > width) throw new IllegalArgumentException("logo 的宽度应该小于等于二维码宽度的 1/3");
        if(logoImage.getHeight() * 3 > height) throw new IllegalArgumentException("logo 的高度应该小于等于二维码高度的 1/3");
    }
}
